import {
    EventDispatcher,
    MOUSE,
    Quaternion,
    Vector2,
    Vector3
} from './three.module.js';

const _changeEvent = { type: 'change' };
const _startEvent = { type: 'start' };
const _endEvent = { type: 'end' };

class TrackballControls extends EventDispatcher {

    constructor(object, domElement) {

        super();

        if (domElement === undefined) console.warn('THREE.TrackballControls: The second parameter "domElement" is now mandatory.');
        if (domElement === document) console.error('THREE.TrackballControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.');

        const scope = this;
        const STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 };

        this.object = object;
        this.domElement = domElement;
        this.domElement.style.touchAction = 'none'; // disable touch scroll

        // API

        this.enabled = true;

        this.screen = { left: 0, top: 0, width: 0, height: 0 };

        this.rotateSpeed = 1.0;
        this.zoomSpeed = 1.2;
        this.panSpeed = 0.3;

        this.noRotate = false;
        this.noZoom = false;
        this.noPan = false;

        this.staticMoving = false;
        this.dynamicDampingFactor = 0.2;

        this.minDistance = 0;
        this.maxDistance = Infinity;

        this.keys = ['KeyA' /*A*/, 'KeyS' /*S*/, 'KeyD' /*D*/];

        this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN };

        // internals

        this.target = new Vector3();

        const EPS = 0.000001;

        const lastPosition = new Vector3();
        let lastZoom = 1;

        let _state = STATE.NONE,
            _keyState = STATE.NONE,

            _touchZoomDistanceStart = 0,
            _touchZoomDistanceEnd = 0,

            _lastAngle = 0;

        const _eye = new Vector3(),

            _movePrev = new Vector2(),
            _moveCurr = new Vector2(),

            _lastAxis = new Vector3(),

            _zoomStart = new Vector2(),
            _zoomEnd = new Vector2(),

            _panStart = new Vector2(),
            _panEnd = new Vector2(),

            _pointers = [],
            _pointerPositions = {};

        // for reset

        this.target0 = this.target.clone();
        this.position0 = this.object.position.clone();
        this.up0 = this.object.up.clone();
        this.zoom0 = this.object.zoom;

        // methods

        this.handleResize = function () {

            const box = scope.domElement.getBoundingClientRect();
            // adjustments come from similar code in the jquery offset() function
            const d = scope.domElement.ownerDocument.documentElement;
            scope.screen.left = box.left + window.pageXOffset - d.clientLeft;
            scope.screen.top = box.top + window.pageYOffset - d.clientTop;
            scope.screen.width = box.width;
            scope.screen.height = box.height;

        };

        const getMouseOnScreen = (function () {

            const vector = new Vector2();

            return function getMouseOnScreen(pageX, pageY) {

                vector.set(
                    (pageX - scope.screen.left) / scope.screen.width,
                    (pageY - scope.screen.top) / scope.screen.height
                );

                return vector;

            };

        }());

        const getMouseOnCircle = (function () {

            const vector = new Vector2();

            return function getMouseOnCircle(pageX, pageY) {

                vector.set(
                    ((pageX - scope.screen.width * 0.5 - scope.screen.left) / (scope.screen.width * 0.5)),
                    ((scope.screen.height + 2 * (scope.screen.top - pageY)) / scope.screen.width) // screen.width intentional
                );

                return vector;

            };

        }());

        this.rotateCamera = (function () {

            const axis = new Vector3(),
                quaternion = new Quaternion(),
                eyeDirection = new Vector3(),
                objectUpDirection = new Vector3(),
                objectSidewaysDirection = new Vector3(),
                moveDirection = new Vector3();

            return function rotateCamera() {

                moveDirection.set(_moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0);
                let angle = moveDirection.length();

                if (angle) {

                    _eye.copy(scope.object.position).sub(scope.target);

                    eyeDirection.copy(_eye).normalize();
                    objectUpDirection.copy(scope.object.up).normalize();
                    objectSidewaysDirection.crossVectors(objectUpDirection, eyeDirection).normalize();

                    objectUpDirection.setLength(_moveCurr.y - _movePrev.y);
                    objectSidewaysDirection.setLength(_moveCurr.x - _movePrev.x);

                    moveDirection.copy(objectUpDirection.add(objectSidewaysDirection));

                    axis.crossVectors(moveDirection, _eye).normalize();

                    angle *= scope.rotateSpeed;
                    quaternion.setFromAxisAngle(axis, angle);

                    _eye.applyQuaternion(quaternion);
                    scope.object.up.applyQuaternion(quaternion);

                    _lastAxis.copy(axis);
                    _lastAngle = angle;

                } else if (!scope.staticMoving && _lastAngle) {

                    _lastAngle *= Math.sqrt(1.0 - scope.dynamicDampingFactor);
                    _eye.copy(scope.object.position).sub(scope.target);
                    quaternion.setFromAxisAngle(_lastAxis, _lastAngle);
                    _eye.applyQuaternion(quaternion);
                    scope.object.up.applyQuaternion(quaternion);

                }

                _movePrev.copy(_moveCurr);

            };

        }());


        this.zoomCamera = function () {

            let factor;

            if (_state === STATE.TOUCH_ZOOM_PAN) {

                factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
                _touchZoomDistanceStart = _touchZoomDistanceEnd;

                if (scope.object.isPerspectiveCamera) {

                    _eye.multiplyScalar(factor);

                } else if (scope.object.isOrthographicCamera) {

                    scope.object.zoom /= factor;
                    scope.object.updateProjectionMatrix();

                } else {

                    console.warn('THREE.TrackballControls: Unsupported camera type');

                }

            } else {

                factor = 1.0 + (_zoomEnd.y - _zoomStart.y) * scope.zoomSpeed;

                if (factor !== 1.0 && factor > 0.0) {

                    if (scope.object.isPerspectiveCamera) {

                        _eye.multiplyScalar(factor);

                    } else if (scope.object.isOrthographicCamera) {

                        scope.object.zoom /= factor;
                        scope.object.updateProjectionMatrix();

                    } else {

                        console.warn('THREE.TrackballControls: Unsupported camera type');

                    }

                }

                if (scope.staticMoving) {

                    _zoomStart.copy(_zoomEnd);

                } else {

                    _zoomStart.y += (_zoomEnd.y - _zoomStart.y) * this.dynamicDampingFactor;

                }

            }

        };

        this.panCamera = (function () {

            const mouseChange = new Vector2(),
                objectUp = new Vector3(),
                pan = new Vector3();

            return function panCamera() {

                mouseChange.copy(_panEnd).sub(_panStart);

                if (mouseChange.lengthSq()) {

                    if (scope.object.isOrthographicCamera) {

                        const scale_x = (scope.object.right - scope.object.left) / scope.object.zoom / scope.domElement.clientWidth;
                        const scale_y = (scope.object.top - scope.object.bottom) / scope.object.zoom / scope.domElement.clientWidth;

                        mouseChange.x *= scale_x;
                        mouseChange.y *= scale_y;

                    }

                    mouseChange.multiplyScalar(_eye.length() * scope.panSpeed);

                    pan.copy(_eye).cross(scope.object.up).setLength(mouseChange.x);
                    pan.add(objectUp.copy(scope.object.up).setLength(mouseChange.y));

                    scope.object.position.add(pan);
                    scope.target.add(pan);

                    if (scope.staticMoving) {

                        _panStart.copy(_panEnd);

                    } else {

                        _panStart.add(mouseChange.subVectors(_panEnd, _panStart).multiplyScalar(scope.dynamicDampingFactor));

                    }

                }

            };

        }());

        this.checkDistances = function () {

            if (!scope.noZoom || !scope.noPan) {

                if (_eye.lengthSq() > scope.maxDistance * scope.maxDistance) {

                    scope.object.position.addVectors(scope.target, _eye.setLength(scope.maxDistance));
                    _zoomStart.copy(_zoomEnd);

                }

                if (_eye.lengthSq() < scope.minDistance * scope.minDistance) {

                    scope.object.position.addVectors(scope.target, _eye.setLength(scope.minDistance));
                    _zoomStart.copy(_zoomEnd);

                }

            }

        };

        this.update = function () {

            _eye.subVectors(scope.object.position, scope.target);

            if (!scope.noRotate) {

                scope.rotateCamera();

            }

            if (!scope.noZoom) {

                scope.zoomCamera();

            }

            if (!scope.noPan) {

                scope.panCamera();

            }

            scope.object.position.addVectors(scope.target, _eye);

            if (scope.object.isPerspectiveCamera) {

                scope.checkDistances();

                scope.object.lookAt(scope.target);

                if (lastPosition.distanceToSquared(scope.object.position) > EPS) {

                    scope.dispatchEvent(_changeEvent);

                    lastPosition.copy(scope.object.position);

                }

            } else if (scope.object.isOrthographicCamera) {

                scope.object.lookAt(scope.target);

                if (lastPosition.distanceToSquared(scope.object.position) > EPS || lastZoom !== scope.object.zoom) {

                    scope.dispatchEvent(_changeEvent);

                    lastPosition.copy(scope.object.position);
                    lastZoom = scope.object.zoom;

                }

            } else {

                console.warn('THREE.TrackballControls: Unsupported camera type');

            }

        };

        this.reset = function () {

            _state = STATE.NONE;
            _keyState = STATE.NONE;

            scope.target.copy(scope.target0);
            scope.object.position.copy(scope.position0);
            scope.object.up.copy(scope.up0);
            scope.object.zoom = scope.zoom0;

            scope.object.updateProjectionMatrix();

            _eye.subVectors(scope.object.position, scope.target);

            scope.object.lookAt(scope.target);

            scope.dispatchEvent(_changeEvent);

            lastPosition.copy(scope.object.position);
            lastZoom = scope.object.zoom;

        };

        // listeners

        function onPointerDown(event) {

            if (scope.enabled === false) return;

            if (_pointers.length === 0) {

                scope.domElement.setPointerCapture(event.pointerId);

                scope.domElement.addEventListener('pointermove', onPointerMove);
                scope.domElement.addEventListener('pointerup', onPointerUp);

            }

            //

            addPointer(event);

            if (event.pointerType === 'touch') {

                onTouchStart(event);

            } else {

                onMouseDown(event);

            }

        }

        function onPointerMove(event) {

            if (scope.enabled === false) return;

            if (event.pointerType === 'touch') {

                onTouchMove(event);

            } else {

                onMouseMove(event);

            }

        }

        function onPointerUp(event) {

            if (scope.enabled === false) return;

            if (event.pointerType === 'touch') {

                onTouchEnd(event);

            } else {

                onMouseUp();

            }

            //

            removePointer(event);

            if (_pointers.length === 0) {

                scope.domElement.releasePointerCapture(event.pointerId);

                scope.domElement.removeEventListener('pointermove', onPointerMove);
                scope.domElement.removeEventListener('pointerup', onPointerUp);

            }


        }

        function onPointerCancel(event) {

            removePointer(event);

        }

        function keydown(event) {

            if (scope.enabled === false) return;

            window.removeEventListener('keydown', keydown);

            if (_keyState !== STATE.NONE) {

                return;

            } else if (event.code === scope.keys[STATE.ROTATE] && !scope.noRotate) {

                _keyState = STATE.ROTATE;

            } else if (event.code === scope.keys[STATE.ZOOM] && !scope.noZoom) {

                _keyState = STATE.ZOOM;

            } else if (event.code === scope.keys[STATE.PAN] && !scope.noPan) {

                _keyState = STATE.PAN;

            }

        }

        function keyup() {

            if (scope.enabled === false) return;

            _keyState = STATE.NONE;

            window.addEventListener('keydown', keydown);

        }

        function onMouseDown(event) {

            if (_state === STATE.NONE) {

                switch (event.button) {

                    case scope.mouseButtons.LEFT:
                        _state = STATE.ROTATE;
                        break;

                    case scope.mouseButtons.MIDDLE:
                        _state = STATE.ZOOM;
                        break;

                    case scope.mouseButtons.RIGHT:
                        _state = STATE.PAN;
                        break;

                    default:
                        _state = STATE.NONE;

                }

            }

            const state = (_keyState !== STATE.NONE) ? _keyState : _state;

            if (state === STATE.ROTATE && !scope.noRotate) {

                _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY));
                _movePrev.copy(_moveCurr);

            } else if (state === STATE.ZOOM && !scope.noZoom) {

                _zoomStart.copy(getMouseOnScreen(event.pageX, event.pageY));
                _zoomEnd.copy(_zoomStart);

            } else if (state === STATE.PAN && !scope.noPan) {

                _panStart.copy(getMouseOnScreen(event.pageX, event.pageY));
                _panEnd.copy(_panStart);

            }

            scope.dispatchEvent(_startEvent);

        }

        function onMouseMove(event) {

            const state = (_keyState !== STATE.NONE) ? _keyState : _state;

            if (state === STATE.ROTATE && !scope.noRotate) {

                _movePrev.copy(_moveCurr);
                _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY));

            } else if (state === STATE.ZOOM && !scope.noZoom) {

                _zoomEnd.copy(getMouseOnScreen(event.pageX, event.pageY));

            } else if (state === STATE.PAN && !scope.noPan) {

                _panEnd.copy(getMouseOnScreen(event.pageX, event.pageY));

            }

        }

        function onMouseUp() {

            _state = STATE.NONE;

            scope.dispatchEvent(_endEvent);

        }

        function onMouseWheel(event) {

            if (scope.enabled === false) return;

            if (scope.noZoom === true) return;

            event.preventDefault();

            switch (event.deltaMode) {

                case 2:
                    // Zoom in pages
                    _zoomStart.y -= event.deltaY * 0.025;
                    break;

                case 1:
                    // Zoom in lines
                    _zoomStart.y -= event.deltaY * 0.01;
                    break;

                default:
                    // undefined, 0, assume pixels
                    _zoomStart.y -= event.deltaY * 0.00025;
                    break;

            }

            scope.dispatchEvent(_startEvent);
            scope.dispatchEvent(_endEvent);

        }

        function onTouchStart(event) {

            trackPointer(event);

            switch (_pointers.length) {

                case 1:
                    _state = STATE.TOUCH_ROTATE;
                    _moveCurr.copy(getMouseOnCircle(_pointers[0].pageX, _pointers[0].pageY));
                    _movePrev.copy(_moveCurr);
                    break;

                default: // 2 or more
                    _state = STATE.TOUCH_ZOOM_PAN;
                    const dx = _pointers[0].pageX - _pointers[1].pageX;
                    const dy = _pointers[0].pageY - _pointers[1].pageY;
                    _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt(dx * dx + dy * dy);

                    const x = (_pointers[0].pageX + _pointers[1].pageX) / 2;
                    const y = (_pointers[0].pageY + _pointers[1].pageY) / 2;
                    _panStart.copy(getMouseOnScreen(x, y));
                    _panEnd.copy(_panStart);
                    break;

            }

            scope.dispatchEvent(_startEvent);

        }

        function onTouchMove(event) {

            trackPointer(event);

            switch (_pointers.length) {

                case 1:
                    _movePrev.copy(_moveCurr);
                    _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY));
                    break;

                default: // 2 or more

                    const position = getSecondPointerPosition(event);

                    const dx = event.pageX - position.x;
                    const dy = event.pageY - position.y;
                    _touchZoomDistanceEnd = Math.sqrt(dx * dx + dy * dy);

                    const x = (event.pageX + position.x) / 2;
                    const y = (event.pageY + position.y) / 2;
                    _panEnd.copy(getMouseOnScreen(x, y));
                    break;

            }

        }

        function onTouchEnd(event) {

            switch (_pointers.length) {

                case 0:
                    _state = STATE.NONE;
                    break;

                case 1:
                    _state = STATE.TOUCH_ROTATE;
                    _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY));
                    _movePrev.copy(_moveCurr);
                    break;

                case 2:
                    _state = STATE.TOUCH_ZOOM_PAN;
                    _moveCurr.copy(getMouseOnCircle(event.pageX - _movePrev.pageX, event.pageY - _movePrev.pageY));
                    _movePrev.copy(_moveCurr);
                    break;

            }

            scope.dispatchEvent(_endEvent);

        }

        function contextmenu(event) {

            if (scope.enabled === false) return;

            event.preventDefault();

        }

        function addPointer(event) {

            _pointers.push(event);

        }

        function removePointer(event) {

            delete _pointerPositions[event.pointerId];

            for (let i = 0; i < _pointers.length; i++) {

                if (_pointers[i].pointerId == event.pointerId) {

                    _pointers.splice(i, 1);
                    return;

                }

            }

        }

        function trackPointer(event) {

            let position = _pointerPositions[event.pointerId];

            if (position === undefined) {

                position = new Vector2();
                _pointerPositions[event.pointerId] = position;

            }

            position.set(event.pageX, event.pageY);

        }

        function getSecondPointerPosition(event) {

            const pointer = (event.pointerId === _pointers[0].pointerId) ? _pointers[1] : _pointers[0];

            return _pointerPositions[pointer.pointerId];

        }

        this.dispose = function () {

            scope.domElement.removeEventListener('contextmenu', contextmenu);

            scope.domElement.removeEventListener('pointerdown', onPointerDown);
            scope.domElement.removeEventListener('pointercancel', onPointerCancel);
            scope.domElement.removeEventListener('wheel', onMouseWheel);

            scope.domElement.removeEventListener('pointermove', onPointerMove);
            scope.domElement.removeEventListener('pointerup', onPointerUp);

            window.removeEventListener('keydown', keydown);
            window.removeEventListener('keyup', keyup);

        };

        this.domElement.addEventListener('contextmenu', contextmenu);

        this.domElement.addEventListener('pointerdown', onPointerDown);
        this.domElement.addEventListener('pointercancel', onPointerCancel);
        this.domElement.addEventListener('wheel', onMouseWheel, { passive: false });


        window.addEventListener('keydown', keydown);
        window.addEventListener('keyup', keyup);

        this.handleResize();

        // force an update at start
        this.update();

    }

}

export { TrackballControls };